<?php
/**
 * @license http://www.gnu.org/licenses/gpl.html GNU GPL, version 3 or later
 * @package FrontPress
 * @subpackage Thing
 */

/**
 * Base class for all objects used in FrontPress-based software.
 *
 * You must extend this with your own subclasses to use it.
 *
 * The variables defined in the subclass are important - they will be used
 * when determining what data is available.
 *
 * @since   2010-07-27-r1
 * @version $Id: class.fp-thing.php 52 2010-08-03 22:02:07Z Nightgunner5 $
 */
abstract class FP_Thing {
	/**
	 * You will need to put a reference to your database here.
	 *
	 * <code>
	 * FP_Thing::$db =& $my_bpdb_object;
	 * </code>
	 *
	 * @var BPDB
	 */
	static $db;

	/**
	 * Stores the names of all subclasses of FP_Thing.
	 *
	 * @access private
	 * @var array
	 */
	static $things = array();

	/**
	 * The table in the database where this FP_Thing's data is stored.
	 * This must be defined in subclasses of FP_Thing.
	 *
	 * @var string
	 */
	static $__table;
	/**
	 * @var bool
	 */
	static $__has_meta = false;
	/**
	 * If this is 'meta', meta will be appended from the meta table
	 * using the schema found in {@link FP_Installer::get_meta_schema()}.
	 * Otherwise, the meta will be obtained using the schema found
	 * in {@link FP_Installer::get_user_schema()} with user_id changed
	 * to the current class's identifier.
	 *
	 * @see FP_Thing::$__has_meta
	 * @var string
	 */
	static $__meta_table = 'meta';

	/**
	 * Construct a new FP_Thing object.
	 *
	 * <code>
	 * $thing = new My_Thing( array(
	 *     'key' => 'value',
	 *     'foo' => 'bar'
	 * ) );
	 * </code>
	 *
	 * If a key matches the name of a subclass of FP_Thing, this function will
	 * attempt to retrieve it from the database or cache.
	 *
	 * @param object|array $data The data represented in this instance of FP_Thing.
	 * @param bool $from_db Internal - caches $data
	 */
	final function __construct( $data = null, $from_db = false ) {
		$this->__class = get_class( $this );
		if ( !FP_Thing::$db )
			trigger_error( 'You must define FP_Thing::$db before using FP_Thing.', E_USER_ERROR );
		if ( !static::$__table )
			trigger_error( '$__table must be defined in subclasses of FP_Thing.', E_USER_ERROR );

		if ( empty( FP_Thing::$things ) ) {
			foreach ( get_declared_classes() as $class ) {
				if ( is_subclass_of( $class, __CLASS__ ) ) {
					FP_Thing::$things[preg_replace( '/^[a-z]+_/', '', strtolower( $class ) )] = $class;
				}
			}
		}

		if ( $data ) {
			if ( is_object( $data ) )
				$data = get_object_vars( $data );

			$vars = array_keys( get_class_vars( $this->__class ) );

			foreach ( $vars as $var ) {
				if ( isset( $data[$var] ) ) {
					$this->$var = is_string( $data[$var] ) && ( $data[$var] == 'N;' || strpos( $data[$var], ':' ) !== false ) ?
						maybe_unserialize( $data[$var] ) : $data[$var];
				}
			}

			if ( $from_db )
				wp_cache_add( $data[$this->get_id_field()], wp_clone( $this ), $this->__class );
		}
	}

	/**
	 * Get one or more FP_Thing from the database or cache
	 *
	 * @return FP_Thing|FP_Things|void FP_Thing or void when the limit is 1. Otherwise, FP_Things.
	 */
	final static function __callStatic( $method, $args ) {
		if ( !FP_Thing::$db )
			trigger_error( 'You must define FP_Thing::$db before using FP_Thing.', E_USER_ERROR );
		if ( !static::$__table )
			trigger_error( '$__table must be defined in subclasses of FP_Thing.', E_USER_ERROR );

		global $wp_object_cache;

		$class = get_called_class();
		$vars = get_class_vars( $class );

		$_method = $method;

		$one = false;
		$limit = 0;
		$start = 0;

		$id_field = static::get_id_field();
		if ( $id_field && ( $method == 'one_by_' . $id_field || $method == 'get' ) ) {
			$method = 'one_by_' . $id_field;

			if ( $cached = wp_cache_get( $args[0], $class ) )
				return static::_append_meta( wp_clone( $cached ) );
			if ( wp_cache_get( $args[0], $class . '__not_found' ) )
				return null;
		}

		if ( substr( $method, 0, 7 ) == 'one_by_' ) {
			$one = true;
			$method = substr( $method, 4 );
			$limit = 1;
		} elseif ( substr( $method, -11 ) == '_with_limit' ) {
			$method = substr( $method, 0, -11 );
			$limit = array_pop( $args );
		} elseif ( substr( $method, -20 ) == '_with_page_and_limit' ) {
			$method = substr( $method, 0, -20 );
			$limit = array_pop( $args );
			$start = max( 0, ( array_pop( $args ) - 1 ) * $limit );
		}

		$vars_reg = '(?:' . str_replace( array( '/', '_id', '_slug' ), array( '\\/', '(?:_id)?', '(?:_slug)?' ), implode( '|', array_map( 'preg_quote', array_keys( $vars ) ) ) ) . ')';
		if ( ( $all_things = wp_cache_get( $class, 'FP_Thing__all' ) ) && preg_match( '/^by_(' . $vars_reg . '(?:_not)?(?:_like)?(?:_and_' . $vars_reg . '(?:_not)?(?:_like)?)*)$/', $method, $_by ) ) {
			$filtered = $all_things;

			$i = 0;
			foreach ( explode( '_and_', $_by[1] ) as $by ) {
				foreach ( array( '', '_not_like', '_like', '_not' ) as $suffix ) {
					$var = preg_replace( '/' . $suffix . '$/', '', $by );
					if ( array_key_exists( $var, $vars ) )
						$filtered = call_user_func( array( &$filtered, 'filter' . $suffix ), $var, $args[$i] );
					elseif ( array_key_exists( $var . '_id', $vars ) )
						$filtered = call_user_func( array( &$filtered, 'filter' . $suffix ), $var . '_id', $args[$i] );
					elseif ( array_key_exists( $var . '_slug', $vars ) )
						$filtered = call_user_func( array( &$filtered, 'filter' . $suffix ), $var . '_slug', $args[$i] );
					else
						continue;
					break;
				}
				$i++;
			}

			if ( $limit ) {
				$filtered = new FP_Things( array_slice( $filtered->get(), $start, $limit ), count( $filtered ) );
			}

			static::_append_meta( $filtered->get() );

			return $one ? $filtered[0] : $filtered;
		}

		if ( preg_match( '/^by_(' . $vars_reg . '(?:_like)?(?:_(?:and|or)_' . $vars_reg . '(?:_like)?)*)$/', $method, $by ) ) {
			$by = preg_split( '/(_and_|_or_)/', str_replace( array( '_and_id', '_and_slug', '_id', '_slug', '_and_i-d', '_and_sl-ug' ),
				array( '_and_i-d', '_and_sl-ug', '', '', '_and_id', '_and_slug' ), $by[1] ), -1, PREG_SPLIT_DELIM_CAPTURE );

			if ( $limit && !$one )
				$query = 'SELECT SQL_CALC_FOUND_ROWS * FROM `' . FP_Thing::$db->{static::$__table} . '` WHERE ';
			else
				$query = 'SELECT * FROM `' . FP_Thing::$db->{static::$__table} . '` WHERE ';

			if ( static::base_where() )
				$query .= static::base_where() . ' AND ';

			$arg = 0;
			foreach ( $by as $item ) {
				if ( $item == '_and_' ) {
					$query .= ' AND ';
				} elseif ( $item == '_or_' ) {
					$query .= ' OR ';
				} else {
					$compare = '=';
					if ( !array_key_exists( $item, $vars ) && substr( $item, -5 ) == '_like' ) {
						$item = substr( $item, 0, -5 );
						$compare = 'LIKE';
					}
					if ( !array_key_exists( $item, $vars ) && substr( $item, -4 ) == '_not' ) {
						$item = substr( $item, 0, -4 );
						$compare = $compare == 'LIKE' ? 'NOT LIKE' : '!=';
					}
					if ( array_key_exists( $item, $vars ) )
						$key = '`' . $item . '`';
					elseif ( array_key_exists( $item . '_id', $vars ) )
						$key = '`' . $item . '_id`';
					elseif ( array_key_exists( $item . '_slug', $vars ) )
						$key = '`' . $item . '_slug`';
					else // Should never happen.
						$key = '`' . $item . '`';

					$query .= $key;

					if ( is_null( $args[$arg] ) ) {
						$query .= ' IS' . ( in_array( $compare, array( 'NOT LIKE', '!=' ) ) ? ' NOT' : '' ) . ' NULL';
					} elseif ( is_a( $args[$arg], 'FP_Thing' ) ) {
						if ( isset( $args[$arg]->{$args[$arg]->get_id_field()} ) ) {
							$query .= ' ' . $compare . " '" . FP_Thing::$db->escape( $args[$arg]->{$args[$arg]->get_id_field()} ) . "'";
						} else {
							trigger_error( 'Could not determine the value of ' . $item . '.', E_USER_ERROR );
						}
					} elseif ( is_a( $args[$arg], 'FP_Things' ) ) {
						if ( !count( $args[$arg] ) ) {
							$query .= ' IN (NULL)';
						} elseif ( isset( $args[$arg]->{$args[$arg][0]->get_id_field()} ) ) {
							$query = substr( $query, 0, -strlen( $key ) );
							$pre = '(' . $key;
							foreach ( $args[$arg]->{$args[$arg][0]->get_id_field()} as $val ) {
								$query .= $pre . ' ' . $compare . " '" . FP_Thing::$db->escape( $val ) . "'";
								$pre = ' OR ' . $key;
							}
							$query .= ')';
						} else {
							trigger_error( 'Could not determine the value of ' . $item . '.', E_USER_ERROR );
						}
					} elseif ( is_array( $args[$arg] ) ) {
						if ( $args[$arg] ) {
							$query = substr( $query, 0, -strlen( $key ) );
							$pre = '(' . $key;
							foreach ( $args[$arg] as $val ) {
								$query .= $pre . ' ' . $compare . " '" . FP_Thing::$db->escape( $val ) . "'";
								$pre = ' OR ' . $key;
							}
							$query .= ')';
						} else {
							$query .= ' IN (NULL)';
						}
					} else {
						$query .= ' ' . $compare . " '" . FP_Thing::$db->escape( maybe_serialize( $args[$arg] ) ) . "'";
					}

					$arg++;
				}
			}

			if ( $limit )
				$query .= ' LIMIT ' . (int) $start . ', ' . (int) $limit;

			if ( $_method == 'get' ) {
				$_results = FP_Thing::$db->get_results( $query );
				if ( !$_results )
					wp_cache_add( $args[0], true, $class . '__not_found' );
			} else {
				if ( false === $_results = wp_cache_get( sha1( $query ), $class . '__query' ) ) {
					$_results = FP_Thing::$db->get_results( $query );
					if ( $limit )
						$_results[] = FP_Thing::$db->get_var( 'SELECT FOUND_ROWS()' );
					else
						$_results[] = count( $_results );
					wp_cache_add( sha1( $query ), $_results, $class . '__query' );
				}
				$found = array_pop( $_results );
			}
			$results = array();
			if ( method_exists( $wp_object_cache, 'get_mc' ) ) {
				$wp_object_cache->no_mc_groups[] = '_group_keys';
			}
			foreach ( $_results as $result ) {
				$results[] = new $class( $result, true );
			}
			if ( method_exists( $wp_object_cache, 'get_mc' ) ) {
				array_pop( $wp_object_cache->no_mc_groups );
				wp_cache_replace( 1, $wp_object_cache->cache['_group_keys:1'], '_group_keys' );
			}

			if ( $one )
				return static::_append_meta( $results[0] );

			return new FP_Things( static::_append_meta( $results ), $found );
		}

		if ( $method == 'all' ) {
			if ( $all_things = wp_cache_get( $class, 'FP_Thing__all' ) ) {
				if ( $limit ) {
					return new FP_Things( static::_append_meta( array_slice( $all_things->get(), $start, $limit ) ), $all_things->total_found_rows );
				}
				$all_things = wp_clone( $all_things );
				static::_append_meta( $all_things->get() );
				return $all_things;
			}

			if ( $limit )
				$query = 'SELECT SQL_CALC_FOUND_ROWS * FROM `' . FP_Thing::$db->{static::$__table} . '`';
			else
				$query = 'SELECT * FROM `' . FP_Thing::$db->{static::$__table} . '`';

			if ( static::base_where() )
				$query .= ' WHERE ' . static::base_where();

			if ( $limit )
				$query .= ' LIMIT ' . (int) $start . ', ' . (int) $limit;

			if ( false === $_results = wp_cache_get( sha1( $query ), $class . '__query' ) ) {
				$_results = FP_Thing::$db->get_results( $query );
				if ( $limit )
					$_results[] = FP_Thing::$db->get_var( 'SELECT FOUND_ROWS()' );
				else
					$_results[] = count( $_results );
				wp_cache_add( sha1( $query ), $_results, $class . '__query' );
			}
			$found = array_pop( $_results );
			$results = array();
			if ( method_exists( $wp_object_cache, 'get_mc' ) ) {
				$wp_object_cache->no_mc_groups[] = '_group_keys';
			}
			foreach ( $_results as $result ) {
				$results[] = new $class( $result, true );
			}
			if ( method_exists( $wp_object_cache, 'get_mc' ) ) {
				array_pop( $wp_object_cache->no_mc_groups );
				wp_cache_replace( 1, $wp_object_cache->cache['_group_keys:1'], '_group_keys' );
			}

			$return = new FP_Things( static::_append_meta( $results ), $found );
			if ( !$limit ) {
				wp_cache_add( $class, $return, 'FP_Thing__all' );
			}

			return $return;
		}

		trigger_error( 'Call to undefined method ' . $class . '::' . $_method . '()', E_USER_WARNING );
	}

	/**
	 * Append meta to objects
	 *
	 * @param $objects
	 * @access private
	 */
	final static function _append_meta( $objects ) {
		if ( !$objects )
			return $objects;

		$class = get_called_class();

		$first_only = false;
		if ( !is_array( $objects ) ) {
			$first_only = true;
			$objects = array( $objects );
		}

		$_objects    = array();
		$ids         = array();
		$sub_objects = array();
		foreach ( $objects as &$object ) {
			if ( !$object )
				continue;

			$ids[] = $object->{$object->get_id_field()};
			$_objects[$object->{$object->get_id_field()}] =& $object;
			foreach ( $object as $var => $value ) {
				if ( is_a( $value, 'FP_Thing' ) )
					$value = $value->{$value->get_id_field()};

				if ( in_array( $var, array_keys( FP_Thing::$things ) ) ) {
					$sub_objects[FP_Thing::$things[$var]]['id'][$object->{$object->get_id_field()}][$var] = $value;
				} elseif ( in_array( $_var = preg_replace( '/_id$/', '', $var ), array_keys( FP_Thing::$things ) ) ) {
					$sub_objects[FP_Thing::$things[$_var]]['id'][$object->{$object->get_id_field()}][$_var] = $value;
				} elseif ( in_array( $_var = preg_replace( '/_ID$/', '', $var ), array_keys( FP_Thing::$things ) ) ) {
					$sub_objects[FP_Thing::$things[$_var]]['ID'][$object->{$object->get_id_field()}][$_var] = $value;
				} elseif ( in_array( $_var = preg_replace( '/_slug$/', '', $var ), array_keys( FP_Thing::$things ) ) ) {
					$sub_objects[FP_Thing::$things[$_var]]['slug'][$object->{$object->get_id_field()}][$_var] = $value;
				}

				if ( substr( $var, 0, 7 ) == 'parent_' ) {
					$_var = substr( $var, 7 );
					if ( in_array( $_var, array_keys( FP_Thing::$things ) ) ) {
						$sub_objects[FP_Thing::$things[$_var]]['id'][$object->{$object->get_id_field()}]['parent_' . $_var] = $value;
					} elseif ( in_array( $__var = preg_replace( '/_id$/', '', $_var ), array_keys( FP_Thing::$things ) ) ) {
						$sub_objects[FP_Thing::$things[$__var]]['id'][$object->{$object->get_id_field()}]['parent_' . $__var] = $value;
					} elseif ( in_array( $__var = preg_replace( '/_ID$/', '', $_var ), array_keys( FP_Thing::$things ) ) ) {
						$sub_objects[FP_Thing::$things[$__var]]['ID'][$object->{$object->get_id_field()}]['parent_' . $__var] = $value;
					} elseif ( in_array( $__var = preg_replace( '/_slug$/', '', $_var ), array_keys( FP_Thing::$things ) ) ) {
						$sub_objects[FP_Thing::$things[$__var]]['slug'][$object->{$object->get_id_field()}]['parent_' . $__var] = $value;
					}
				}
			}
		}

		$query = 'SELECT * FROM `' . FP_Thing::$db->{static::$__meta_table} . '` WHERE ';
		if ( static::$__meta_table == 'meta' )
			$query .= '`object_type` = \'' . $class . '\' AND `object_id` IN (\'' . implode( "', '", FP_Thing::$db->escape_deep( $ids ) ) . '\')';
		else
			$query .= '`' . array_search( $class, FP_Thing::$things ) . '_id` IN (\'' . implode( "', '", FP_Thing::$db->escape_deep( $ids ) ) . '\')';

		if ( false === $meta = wp_cache_get( sha1( $query ), $class . '__query' ) ) {
			$meta = FP_Thing::$db->get_results( $query );
			wp_cache_add( sha1( $query ), $meta, $class . '__query' );
		}

		foreach ( $sub_objects as $sub_thing_class => $_sub ) {
			foreach ( $_sub as $type => $sub ) {
				// Cache the data for minimal queries.
				call_user_func( array( $sub_thing_class, 'by_' . $type ), array_unique(
					call_user_func_array( 'array_merge', array_map( 'array_values', $sub ) ) ) );
				foreach ( $sub as $id => $sub_thing_properties ) {
					foreach ( $sub_thing_properties as $property_key => $property_value ) {
						$_objects[$id]->$property_key = call_user_func( array( $sub_thing_class, 'get' ), $property_value );
					}
				}
			}
		}

		if ( !static::$__has_meta )
			return $first_only ? $objects[0] : $objects;

		if ( static::$__meta_table == 'meta' )
			$id_field = 'object_id';
		else
			$id_field = array_search( $class, FP_Thing::$things ) . '_id';

		foreach ( $meta as $m ) {
			if ( !$m->meta_key )
				continue;
			$_objects[$m->{$id_field}]->{$m->meta_key} = $m->meta_value;
		}

		$prefix_len = strlen( FP_Thing::$db->prefix );
		if ( $prefix_len ) {
			foreach ( $_objects as &$object ) {
				foreach ( $object as $key => $val ) {
					if ( substr( $key, 0, $prefix_len ) == FP_Thing::$db->prefix ) {
						$object->{substr( $key, $prefix_len )} = $val;
					}
				}
			}
		}

		return $first_only ? $objects[0] : $objects;
	}

	/**
	 * Save or create the FP_Thing object in the database.
	 *
	 * @return int the number of rows added to or updated in the database
	 */
	final function save() {
		$this->save_subthings();
		$table = FP_Thing::$db->{static::$__table};
		$new = $this->get_values();
		do_action( $this->__class . '_before_save', $this );
		if ( ( $this->get_id_field() ) && $cached = wp_cache_get( $this->{$this->get_id_field()}, $this->__class ) ) {
			$old = $cached->get_values();
			if ( $old == $new ) { // Cache equals current, no need to save.
				return 0;
			}

			$rules = new FP_Validation_Rules( $this );
			$this->validate( $rules );
			if ( !$rules->validate( $new, $old ) ) {
				return 0;
			}

			do_action( $this->__class . '_before_update', $new, $old );
			$ret = FP_Thing::$db->update( $table, $new, $old );
			wp_cache_delete( $this->{$this->get_id_field()}, $this->__class . '__not_found' );
			wp_cache_delete( $this->__class, 'FP_Thing__all' );
			wp_cache_replace( $this->{$this->get_id_field()}, wp_clone( $this ), $this->__class );
			wp_cache_flush( $this->__class . '__query' );
			do_action( $this->__class . '_after_update', $new, $old );
			do_action( $this->__class . '_after_save', $this );
			return $ret;
		} else {
			$rules = new FP_Validation_Rules( $this );
			$this->validate( $rules );
			if ( !$rules->validate_new( $new ) ) {
				return 0;
			}

			do_action( $this->__class . '_before_insert', $new );
			$ret = FP_Thing::$db->insert( $table, $this->get_values() );
			if ( !$this->id && FP_Thing::$db->insert_id )
				$this->id = FP_Thing::$db->insert_id;
			wp_cache_delete( $this->{$this->get_id_field()}, $this->__class . '__not_found' );
			wp_cache_delete( $this->__class, 'FP_Thing__all' );
			wp_cache_add( $this->{$this->get_id_field()}, wp_clone( $this ), $this->__class );
			wp_cache_flush( $this->__class . '__query' );
			do_action( $this->__class . '_after_insert', $new );
			do_action( $this->__class . '_after_save', $this );
			return $ret;
		}
	}

	/**
	 * Set meta on the current object.
	 *
	 * @param string $key
	 * @param mixed $value
	 * @param bool $global
	 * @return int The number of rows changed, or 0 on failure.
	 */
	function set_meta( $key, $value, $global = false ) {
		if ( !static::$__has_meta )
			return 0;

		if ( property_exists( $this->__class, $key ) )
			return 0;

		$_key = $key;
		if ( !$global )
			$key = FP_Thing::$db->prefix . $key;

		$value = maybe_serialize( $_value = $value );
		static::_append_meta( array( &$this ) );
		if ( (string) maybe_serialize( $this->$key ) === (string) $value ) // Zero does not equal null.
			return 0;

		if ( property_exists( $this, $key ) ) {
			$query = 'UPDATE `' . FP_Thing::$db->{static::$__meta_table} . '` SET `meta_value` = \'' . FP_Thing::$db->escape( $value ) . '\' WHERE `meta_key` = \'' . FP_Thing::$db->escape( $key ) . '\' AND ';
			if ( static::$__meta_table == 'meta' )
				$query .= '`object_type` = \'' . FP_Thing::$db->escape( $this->__class ) . '\' AND `object_id` = \'' . FP_Thing::$db->escape( $this->{$this->get_id_field()} ) . '\'';
			else
				$query .= '`' . FP_Thing::$db->escape( array_search( $class, FP_Thing::$things ) . '_id' ) . '` = \'' . FP_Thing::$db->escape( $this->{$this->get_id_field()} ) . '\'';
		} else {
			$query = 'INSERT INTO `' . FP_Thing::$db->{static::$__meta_table} . '` (meta_key, meta_value, ';
			if ( static::$__meta_table == 'meta' )
				$query .= '`object_type`, `object_id`) VALUES (\'' . FP_Thing::$db->escape( $key ) . '\', \'' . FP_Thing::$db->escape( $value ) . '\', \'' . FP_Thing::$db->escape( $this->__class ) . '\', \'' . FP_Thing::$db->escape( $this->{$this->get_id_field()} ) . '\')';
			else
				$query .= '`' . FP_Thing::$db->escape( array_search( $class, FP_Thing::$things ) . '_id' ) . '`) VALUES(\'' . FP_Thing::$db->escape( $key ) . '\', \'' . FP_Thing::$db->escape( $value ) . '\', \'' . FP_Thing::$db->escape( $this->__class ) . '\', \'' . FP_Thing::$db->escape( $this->{$this->get_id_field()} ) . '\')';
		}

		$ret = FP_Thing::$db->query( $query );

		$this->$key = $_value;
		$this->$_key = $_value;

		wp_cache_delete( $this->{$this->get_id_field()}, $this->__class );
		wp_cache_delete( $this->__class, 'FP_Thing__all' );
		wp_cache_flush( $this->__class . '__query' );
		do_action( $this->__class . '_meta_set', $this, $key, $value, $global );

		return $ret;
	}

	function delete_meta( $key ) {
		if ( !static::$__has_meta )
			return 0;

		$query = 'DELETE FROM `' . FP_Thing::$db->{static::$__meta_table} . '` WHERE (`meta_key` = \'' . FP_Thing::$db->escape( $key ) . '\' OR `meta_key` = \'' . FP_Thing::$db->escape( FP_Thing::$db->prefix . $key ) . '\') AND ';
		if ( static::$__meta_table == 'meta' )
			$query .= '`object_type` = \'' . FP_Thing::$db->escape( $this->__class ) . '\' AND `object_id` = \'' . FP_Thing::$db->escape( $this->{$this->get_id_field()} ) . '\'';
		else
			$query .= '`' . FP_Thing::$db->escape( array_search( $class, FP_Thing::$things ) . '_id' ) . '` = \'' . FP_Thing::$db->escape( $this->{$this->get_id_field()} ) . '\'';

		$ret = FP_Thing::$db->query( $query );

		wp_cache_delete( $this->{$this->get_id_field()}, $this->__class );
		wp_cache_delete( $this->__class, 'FP_Thing__all' );
		wp_cache_flush( $this->__class . '__query' );
		do_action( $this->__class . '_meta_delete', $this, $key );

		return $ret;
	}

	/**
	 * Delete an FP_Thing object from the database.
	 *
	 * @return int the number of rows deleted from the database
	 */
	final function delete() {
		$table = FP_Thing::$db->{static::$__table};
		if ( $cached = wp_cache_get( $this->{$this->get_id_field()}, $this->__class ) ) {
			$old = $cached->get_values();
			$new = $this->get_values();
			if ( $old != $new ) {
				return 0;
			}

			// Allow plugins to cancel delete operations
			if ( apply_filters( $this->__class . '_before_delete', false, $new ) )
				return 0;

			$query = '';
			$append = 'DELETE FROM `' . $table . '` WHERE ';

			foreach ( $this->get_values() as $key => $value ) {
				$query .= $append;
				$query .= '`' . $key . '` = \'' . FP_Thing::$db->escape( $value ) . '\'';
				$append = ' AND ';
			}

			$ret = FP_Thing::$db->query( $query );
			wp_cache_add( $this->{$this->get_id_field()}, true, $this->__class . '__not_found' );
			wp_cache_delete( $this->__class, 'FP_Thing__all' );
			wp_cache_delete( $this->{$this->get_id_field()}, $this->__class );
			wp_cache_flush( $this->__class . '__query' );
			do_action( $this->__class . '_after_delete', $new );
			return $ret;
		}
		return 0;
	}

	/**
	 * Save the FP_Thing objects contained in this one recursively.
	 *
	 * @access private
	 */
	private function save_subthings() {
		$values = get_object_vars( $this );

		unset( $values['__table'] );
		unset( $values['__class'] );

		foreach ( $values as $val ) {
			if ( is_a( $val, 'FP_Thing' ) )
				$val->save();
		}
	}

	/**
	 * Get the values of an FP_Thing as a database-ready array.
	 *
	 * @access private
	 * @return array
	 */
	private function get_values() {
		$_values = get_object_vars( $this );
		$allowed_keys = array_keys( get_class_vars( $this->__class ) );

		unset( $_values['__table'] );
		unset( $_values['__class'] );

		$values = array();
		foreach ( $_values as $key => $val ) {
			if ( !in_array( $key, $allowed_keys ) )
				continue;
			if ( is_a( $val, 'FP_Thing' ) && ( $val->get_id_field() ) )
				$values[$key] = $val->{$val->get_id_field()};
			else
				$values[$key] = maybe_serialize( $val );
		}

		return $values;
	}

	/**
	 * Abstract function to validate before saving. If this is
	 * not implemented, there will be no restrictions on what
	 * can and cannot be values.
	 *
	 * @param FP_Validation_Rules $rules
	 */
	function validate( $rules ) {}

	/**
	 * Abstract function to allow limiting of results. This is
	 * useful when multiple types of data are in the same table,
	 * such as options in a meta table.
	 *
	 * @return string
	 */
	function base_where() { return ''; }

	/**
	 * Guess the field used as an identifier. Override this if
	 * the primary identifier is not one of 'id', 'ID', or 'slug'.
	 *
	 * @return string
	 */
	function get_id_field() {
		if ( !$t = $this ) {
			$t = get_called_class();
		}
		if ( property_exists( $t, 'id' ) )
			return 'id';
		if ( property_exists( $t, 'ID' ) )
			return 'ID';
		if ( property_exists( $t, 'slug' ) )
			return 'slug';
	}
}
